package spider

import (
	"context"
	"encoding/json"
	"errors"
	"gitee.com/yysf-xin/dproxy/pkg/app"
	"gitee.com/yysf-xin/dproxy/pkg/corn"
	"gitee.com/yysf-xin/dproxy/pkg/db"
	"gitee.com/yysf-xin/dproxy/pkg/model"
	"gitee.com/yysf-xin/dproxy/pkg/rest"
	"gitee.com/yysf-xin/dproxy/pkg/tools"
	"gitee.com/yysf-xin/dproxy/pkg/v2ray"
	"gitee.com/yysf-xin/dproxy/pkg/v2ray/clash"
	"github.com/go-chi/chi/v5"
	"github.com/golang/protobuf/proto"
	"github.com/robfig/cron/v3"
	core "github.com/v2fly/v2ray-core/v5"
	"github.com/v2fly/v2ray-core/v5/common/protocol"
	"github.com/v2fly/v2ray-core/v5/features/inbound"
	"github.com/v2fly/v2ray-core/v5/features/outbound"
	"github.com/v2fly/v2ray-core/v5/infra/conf/v5cfg"
	"github.com/v2fly/v2ray-core/v5/proxy"
	vhttp "github.com/v2fly/v2ray-core/v5/proxy/http"
	"github.com/v2fly/v2ray-core/v5/proxy/trojan"
	"github.com/v2fly/v2ray-core/v5/proxy/vmess"
	"gopkg.in/yaml.v3"
	"net"
	"net/http"
	"net/url"
	"reflect"
	"strconv"
	"strings"
	"sync"
	"unsafe"
)

type Status struct {
	Curr uint
	Url  string
	*v2ray.Info
}

const (
	ServerIdKey      = "v2flay_server_id"
	ServerAccountKey = "v2flay_account"
)

type V2flay struct {
	DB    *db.Client
	model *v2ray.Model
	conf  model.SysConfig
	hook  v2ray.HookOutHandler
	Status
	task *v2ray.CheckClashTask
	rw   sync.RWMutex
	um   proxy.UserManager
	curr *clash.ConfigClash
	run  bool
	root *clash.RootClash
	v2ray.Dialer
}

func (v *V2flay) String() string {
	return "fly"
}

func (v *V2flay) DoSwitch(id uint) (err error) {
	var info clash.ConfigClash
	err = v.DB.Db.Find(&info, id).Error
	if err != nil {
		return
	}
	oh, err := v2ray.CreateHandler(v.Dialer.Ins, &info)
	if err != nil {
		return
	}
	if oh != nil && !reflect.ValueOf(oh).IsNil() {
		tools.Log.Println("change to id =", id, info.Name, info.Server)
		v.Curr = id
		if v.hook.Handler != nil {
			err = v.hook.Handler.Close()
		}
		v.hook.Handler = oh.(outbound.Handler)
		v.Dialer.Handler = v.hook.Handler
		v.curr = &info
		if !v.run {
			err = v.Start()
		}
	}
	return
}

func (v *V2flay) InitRoute(r chi.Router) {
	r.Get("/enable", v.handlerEnable)
	r.Get("/select", v.Select)
	r.Get("/save", v.save)
	r.HandleFunc("/check", v.CheckClash)
	r.Get("/status", v.GetStatus)
	r.Get("/checkOne", v.checkOne)
	r.Get("/canUse", v.CanUse)
	r.Post("/checkFile", v.checkFile)
}

func (v *V2flay) handlerEnable(w http.ResponseWriter, r *http.Request) {
	vv := r.URL.Query().Get("enable")
	var err error
	if vv == "true" {
		err = v.Start()
	} else {
		err = v.Dialer.Ins.Close()
		v.Stop()
	}
	app.RenderJSON(w, nil, err)
}

func (v *V2flay) Select(w http.ResponseWriter, r *http.Request) {
	id, err := rest.QueryParamId(r, "id")
	if err != nil {
		app.RenderJSON(w, nil, err)
		return
	}
	err = v.DoSwitch(id)
	app.RenderJSON(w, nil, err)
}

func ParseV2Addr(v string) (p proto.Message, addr *url.URL, err error) {
	addr, err = url.Parse(v)
	if err != nil {
		return
	}
	host, port, err := net.SplitHostPort(addr.Host)
	if err != nil {
		return
	}
	tools.Log.Println("start v2fly", addr.Scheme, "addr", addr.Host)

	portI, err := strconv.Atoi(port)
	if err != nil {
		return
	}

	var in = v2ray.Inbound{
		Protocol: addr.Scheme, Port: portI,
		ListenOn: host,
	}
	jsonData, err := json.Marshal(in)
	if err != nil {
		return
	}
	inCfg := &v5cfg.InboundConfig{}
	err = json.Unmarshal(jsonData, inCfg)
	if err != nil {
		return
	}
	p, err = inCfg.BuildV5(context.TODO())

	return
}

type httpServer struct {
	config *vhttp.ServerConfig
}

func NewV2flay(DB *db.Client, config *app.Config, services *app.Services) (v2 *V2flay, err error) {
	if config.File.Db.Upgrade {
		var m = &v2ray.Model{}
		err = db.UpgradeDB(m, DB.Db)
		if err != nil {
			return
		}
	}
	ins, err := v2ray.NewIns()
	if err != nil {
		return
	}

	v2 = &V2flay{
		DB:   DB,
		root: config.File.Clash,
	}
	services.AddService(v2)

	v2.Dialer.Ins = ins

	v2.regTask()

	if config.File.V2ray == nil || len(config.File.V2ray) == 0 {
		return
	}

	inH := ins.GetFeature(inbound.ManagerType()).(inbound.Manager)
	outH := ins.GetFeature(outbound.ManagerType()).(outbound.Manager)

	var (
		ms proto.Message
		h  interface{}
		u  *url.URL
	)
	for _, v := range config.File.V2ray {
		ms, u, err = ParseV2Addr(v.Addr)
		if err != nil {
			return
		}
		h, err = core.CreateObject(ins, ms)
		if err != nil {
			return
		}
		err = inH.AddHandler(context.Background(), h.(inbound.Handler))
		if err != nil {
			return
		}

		if hh, ok := h.(proxy.GetInbound); ok {
			ss := hh.GetInbound()
			switch ss.(type) {
			case proxy.UserManager:
				v2.initUsers(ss.(proxy.UserManager), u)
			case *vhttp.Server:
				initUsers(ss.(*vhttp.Server), u)
			}
		}

	}

	v2.conf.Name = ServerIdKey
	err = v2.conf.FindByDB(DB.Db)

	var currId int
	if err == nil {
		currId, err = strconv.Atoi(v2.conf.Value)
	} else {
		v2.conf.Value = "0"
		err = DB.Db.Create(&v2.conf).Error
	}

	if err != nil {
		tools.Log.Println(err)
	}

	err = outH.AddHandler(context.Background(), &v2.hook)

	if currId != 0 {
		_ = v2.DoSwitch(uint(currId))
	}

	if err != nil {
		tools.Log.Println(err)
		return
	}

	return v2, err
}

func initUsers(v *vhttp.Server, u *url.URL) {
	if u.User != nil {
		s := (*httpServer)(unsafe.Pointer(reflect.ValueOf(v).Pointer()))
		if s.config.Accounts == nil {
			s.config.Accounts = make(map[string]string)
		}
		p, ok := u.User.Password()
		if !ok {
			p = u.User.Username()
		}
		s.config.Accounts[u.User.Username()] = p
	}

}

func (v *V2flay) Start() error {
	if v.hook.Handler != nil {
		v.run = true
		return v.Dialer.Ins.Start()
	}
	v.run = false
	return errors.New("handler is null")
}

func (v *V2flay) Stop() {
	_ = v.Dialer.Ins.Close()
	v.run = false
}

func (v *V2flay) save(w http.ResponseWriter, r *http.Request) {
	if v.Curr == 0 {
		app.RenderJSON(w, nil, errors.New("没有要保存的"))
		return
	}
	v.conf.Name = ServerIdKey
	v.conf.Value = strconv.Itoa(int(v.Curr))

	v.DB.Db.Save(&v.conf)

	app.RenderJSON(w, v.conf, nil)
}

type query struct {
	Url     string `json:"url" yaml:"url"`
	OnlyNew bool   `json:"new" yaml:"new"`
}

func (v *V2flay) CheckClash(w http.ResponseWriter, r *http.Request) {

	var (
		q   query
		err error
	)
	if r.Method == "GET" {
		q.Url = r.URL.Query().Get("url")
	} else {
		err = app.ReadJSON(r, &q)
	}

	if err != nil {
		app.RenderJSON(w, nil, err)
		return
	}
	if q.Url == "" {
		app.RenderJSON(w, nil, errors.New("无效的请求"))
		return
	}

	task, err := v2ray.LoadClashFromUrl(q.Url)
	if err == nil {
		v.doCheck(task)
	}
	app.RenderJSON(w, nil, err)
	return

}

func (v *V2flay) doCheck(task *v2ray.CheckClashTask) {
	v.rw.Lock()
	if v.task != nil {
		v.task.Stop = true
	}
	v.task = task
	v.rw.Unlock()

	v.Status.Info = &v.task.Info
	v.task.OnlyNew = true
	go v.task.CheckClash(v.Dialer.Ins, v.DB)
}

func (v *V2flay) GetStatus(w http.ResponseWriter, r *http.Request) {
	app.RenderJSON(w, &v.Status, nil)
}

func (v *V2flay) CanUse(w http.ResponseWriter, r *http.Request) {
	var Proxies []*clash.ConfigClash
	rs := v.DB.Db.Raw("select a.id,a.created_at,a.updated_at,a.server,a.port,a.country,a.city,a.name,a.type,"+
		"a.can_use from proxy_clash a where a.can_use > ? order by a.id desc limit ?",
		0, 200).Find(&Proxies)
	app.RenderJSON(w, Proxies, rs.Error)
}

func (v *V2flay) clashYml(w http.ResponseWriter, r *http.Request) {
	var Proxies []*clash.ConfigClash
	_ = v.DB.Db.Raw("select a.* from proxy_clash a where a.can_use > ? order by a.id desc limit ?",
		0, 200).Find(&Proxies).Error

	if Proxies != nil && len(Proxies) > 0 {
		num := 0
		nameMap := map[string]string{}

		for _, vl := range Proxies {
			name := nameMap[vl.Name]
			if name != "" {
				vl.Name = vl.Name + strconv.Itoa(num)
				num++
			}
			nameMap[vl.Name] = vl.Name
		}
	}

	root := clash.RootClash{
		Proxies: Proxies,
	}
	en := yaml.NewEncoder(w)
	en.Encode(&root)
}

func (v *V2flay) regTask() {
	corn.RegJob(corn.ClashTask, "Clash订阅抓取", func(ctl *corn.Control, cfg *model.CornConfig) (j cron.Job, err error) {
		var q query
		err = yaml.Unmarshal([]byte(cfg.Param), &q)
		if err != nil {
			return
		}
		j = &v2ray.CornClashTask{
			CheckClashTask: v2ray.CheckClashTask{
				Target:  &clash.RootClash{},
				OnlyNew: q.OnlyNew,
				Info: v2ray.Info{
					Stop: true,
				},
			},
			Ins: v.Dialer.Ins,
			Db:  v.DB,
			Url: q.Url,
		}

		return
	})

	corn.RegJob(corn.ClashCleanTask, "Clash订阅清理", func(ctl *corn.Control, cfg *model.CornConfig) (j cron.Job, err error) {
		return v2ray.NewClashCleanJon(v.DB, &v.Dialer, v, cfg), nil
	})
}

type passwd struct {
	Username string `yaml:"username"`
	Password string `yaml:"password"`
	Id       string `yaml:"id"`
}

func (v *V2flay) initUsers(um proxy.UserManager, addr *url.URL) {
	if addr.User == nil {
		return
	}
	u := &protocol.MemoryUser{
		Email: addr.User.Username(),
		Level: 1,
	}
	if addr.Scheme == "trojan" {
		account := &trojan.Account{
			Password: addr.User.Username(),
		}
		p, ok := addr.User.Password()
		if ok {
			account.Password = p
		}
		u.Account, _ = account.AsAccount()
	} else if addr.Scheme == "vmess" {
		account := &vmess.Account{
			Id: addr.Query().Get("id"),
		}
		u.Account, _ = account.AsAccount()
	}
	err := um.AddUser(context.TODO(), u)
	if err != nil {
		tools.Log.Println(err)
	}

}

func (v *V2flay) checkOne(w http.ResponseWriter, r *http.Request) {
	id := r.URL.Query().Get("id")

	var cfg clash.ConfigClash
	err := v.DB.Db.Find(&cfg, "id = ?", id).Error
	var result *v2ray.Info
	if err == nil {
		task := v2ray.NewCheckClashTask(&cfg)
		task.CheckClash(v.Dialer.Ins, v.DB)
		result = &task.Info
		if result.Success == 0 {
			err = errors.New("check failed")
		}
	}
	app.RenderJSON(w, result, err)
}

func (v *V2flay) checkFile(w http.ResponseWriter, r *http.Request) {
	f, _, err := r.FormFile("file")
	var task *v2ray.CheckClashTask
	if err == nil {
		task, err = v2ray.LoadClashFromYml(f)
		if err == nil {
			v.doCheck(task)
		}
	}
	app.RenderJSON(w, nil, err)
}

func (v *V2flay) PubRoute(r chi.Router) {
	r.Get("/proxies", v.clashYml)
	r.Get("/config", v.clash)
}

func (v *V2flay) clash(w http.ResponseWriter, r *http.Request) {
	var cfg clash.RootClash
	if v.root != nil {
		cfg = *v.root
	}
	cfg.Rules = []string{"DOMAIN-SUFFIX,google.com,auto"}
	cfg.ProxyGroup = []*clash.Group{
		{
			Name: "auto",
			Type: "select",
			Use: []string{
				"provider1",
			},
			//Proxies: []string{"Proxy"},
		},
	}
	pr := &clash.Provider{
		Type: "http",
		Path: "./provider1.yaml",
	}
	buff := strings.Builder{}
	buff.WriteString("http")

	if r.TLS != nil {
		buff.WriteString("s")
	}
	buff.WriteString("://")
	buff.WriteString(r.Host)
	buff.WriteString(r.RequestURI[:strings.LastIndexByte(r.RequestURI, '/')])
	buff.WriteString("/proxies")

	pr.Url = buff.String()

	pr.Interval = 3600
	cfg.ProxyProvider = map[string]*clash.Provider{
		"provider1": pr,
	}

	_ = yaml.NewEncoder(w).Encode(&cfg)
}
